使用篇丨链路追踪(Tracing)其实很简单:请求轨迹回溯与多维链路筛选
关键路径与多条链路对比
Aliware
本章我们将以业务 Owner(小帅)的视角,逐步了解分布式链路追踪的各种基础用法:小到单次用户请求的异常根因诊断,大到全局系统的强弱依赖梳理,分布式链路追踪都能给予确定性答案。
关键路径
如果一次 Span 调用有 t 段耗时在关键路径上,那么去掉这 t 段耗时,整条链路的总体耗时也会相应的缩短 t 段时间。仍以上面那条链路为例,灰色部分表示关键路径,缩短任意关键路径上的耗时都可以减少整体耗时。此时,我们可以判断 A 接口超时的主要原因是 C 接口导致的。
多条链路对比
单条调用链路只能用来分析各个接口的绝对耗时,而无法得知每个接口的耗时变化情况。但是,绝对耗时长不代表这个接口就一定有问题,比如数据存储接口耗时通常要比单纯的计算接口耗时要长,这种长耗时是合理的,无需特别关注。关联信息回溯
Aliware
通过前面的学习,小帅已经成功掌握了调用链的轨迹回溯能力,可以熟练运用调用链分析性能瓶颈点,快速定位异常的接口。但是,他又遇到了新的困惑,就是找到了异常接口之后,下一步该怎么办?比如 C 接口的耗时从 0.1s 增长到了 2.1s,导致了上游的 A 接口超时。但是仅仅知道这个信息还不够,C 接口耗时增长背后的原因是什么?如何解决这个问题,让它恢复到原来的性能基线?
本地方法栈
自动关联数据
主动关联数据
综合分析
多维度链路筛选
Aliware
(一)基于链路标识 TraceId 的筛选
如何获取 TraceId?
前端请求 Header 或响应体 Response:大部分用户请求都是在端上设备发起的,因此 TraceId 生成的最佳地点也是在端上设备,通过请求 Header 透传给后端服务。因此,我们在通过浏览器开发者模式调试时,就可以获取当前测试请求 Header 中的 TraceId 进行筛选。如果端上设备没有接入分布式链路追踪埋点,也可以将后端服务生成的 TraceId 添加到 Response 响应体中返回给前端。这种方式非常适合前后端联调场景,可以快速找到每一次点击对应的 TraceId,进而分析行为背后的链路轨迹与状态。
网关日志:网关是所有用户请求发往后端服务的代理中转站,可以视为后端服务的入口。在网关的 access.log 访问日志中添加 TraceId,可以帮助我们快速分析每一次异常访问的轨迹与原因。比如一个超时或错误请求,到底是网关自身的原因,还是后端某个服务的原因,可以通过调用链中每个 Span 的状态得到确定性的结论。
应用日志:应用日志可以说是我们最熟悉的一种日志,我们会将各种业务或系统的行为、中间状态和结果,在开发编码的过程中顺手记录到应用日志中,使用起来非常方便。同时,它也是可读性最强的一类日志,即使是非开发运维人员也能大致理解应用日志所表达的含义。因此,我们可以将 TraceId 也记录到应用日志中进行关联,一旦出现某种业务异常,我们可以先通过当前应用的日志定位到报错信息,再通过关联的 TraceId 去追溯该应用上下游依赖的其他信息,最终定位到导致问题出现的根因节点。
组件日志:在分布式系统中,大部分应用都会依赖一些外部组件,比如数据库、消息、配置中心等等。这些外部组件也会经常发生这样或那样的异常,最终影响应用服务的整体可用性。但是,外部组件通常是共用的,有专门的团队进行维护,不受应用 Owner 的控制。因此,一旦出现问题,也很难形成有效的排查回路。此时,我们可以将 TraceId 透传给外部组件,并要求他们在自己的组件日志中进行关联,同时开放组件日志查询权限。举个例子,我们可以通过 SQL Hint 传播链 TraceId,并将其记录到数据库服务端的 Binlog 中,一旦出现慢 SQL 就可以追溯数据库服务端的具体表现,比如一次请求记录数过多,查询语句没有建索引等等。
如何在日志中关联 TraceId?
基于 SDK 手动埋点:链路透传的每个节点都可以获取当前调用生命周期内的上下文信息。最基础的关联方式就是通过 SDK 来手动获取 TraceId,将其作为参数添加至业务日志的输出中。
基于日志模板自动埋点:如果一个存量应用有大量日志需要关联 TraceId,一行行的修改代码添加 TraceId 的改造成本属实有点高,也很难被执行下去。因此,比较成熟的 Tracing 实现框架会提供一种基于日志模板的自动埋点方式,无需修改业务代码就可以在业务日志中批量注入 TraceId,使用起来极为方便。
基于 SDK 手动实现日志与 TraceId 关联示例
1. 打开应用代码工程的 pom.xml 文件,添加对 Jaeger 客户端的依赖(正常情况下该依赖已经被添加,可以跳过)。
<dependency>
<groupId>io.jaegertracing</groupId>
<artifactId>jaeger-client</artifactId>
<version>0.31.0</version>
</dependency>
2. 在日志输出代码前面,先获取当前调用生命周期的 Span 对象,再从上下文中获取 TraceId 标识。
String traceId = GlobalTracer.get().activeSpan().context().toTraceId();
3. 将 TraceId 添加到业务日志中一并输出。
log.error("fail to create order, traceId: {}", traceId);
4. 最终的日志效果如下所示,这样我们就可以根据业务关键词先过滤日志,再通过关联的 TraceId 查询上下游全链路轨迹的信息。
fail to create order, traceId: ee14662c52387763
基于日志模板实现日志与 TraceId 自动关联示例
@Test
public void testMDC() {
MDC.put("userName", "xiaoming");
MDC.put("traceId", GlobalTracer.get().activeSpan().context().toTraceId());
log.info("Just test the MDC!");
}
第二步,在日志配置文件的 Pattern 描述中添加标签变量 %X{userName} 和 %X{traceId}。
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
<encoder>
<pattern>%d{HH:mm:ss} [%thread] %-5level [userName=%X{userName}] [traceId=%X{traceId}] %msg%n</pattern>
<charset>utf-8</charset>
</encoder>
</appender>
这样,我们就完成了 MDC 变量注入的过程,最终日志输出效果如下所示:
15:17:47 [http-nio-80-exec-1] INFO [userName=xiaoming] [traceId=ee14662c52387763] Just test the MDC!
看到这里,细心的读者可能会疑问,MDC 注入不是也需要修改代码么?答案是确实需要,不过好在 Tracing 框架已经提供了简易的关联方式,无需逐行修改日志代码,极大的减少了改造量。比如 Jaeger SDK 提供了 MDCScopeManager 对象,只需要在创建 Tracer 对象时顺便关联上 MDCScopeManager 就可以实现 traceId、spanId 和 sampled 自动注入到 MDC 上下文中,如下所示:
MDCScopeManager scopeManager = new MDCScopeManager.Builder().build();
JaegerTracer tracer = new JaegerTracer.Builder("serviceName").withScopeManager(scopeManager).build();
通过 MDC 机制,有效推动了实际生产环境中应用日志与 Trace 链路的关联,你也快动手试试吧。
日志关联 TraceId 的限制有哪些?
确认类似 MDCScopeManager 初始化的变量注入工作是否完成? 确认日志模板中是否添加 %X{traceId} 变量? 确认当前日志是否在某个调用的生命周期内部,且确保链路上下文不会因为异步线程切换导致丢失。
综上所述,我们可以在系统报错时,快速找到关联的 TraceId,再进行整条链路的轨迹信息回溯,最终定位根因解决问题。但是,如果我们由于各种限制还没有完成 TraceId 的关联,那么该怎么办呢?接下来我们来介绍两种不需要 TraceId 的筛选方法。
(二)基于链路特征的筛选
serviceName=order AND spanName=createOrder AND duration>5s
(三)基于自定义标签的筛选
@GetMapping("/createOrder")
public ApiResponse createOrder(@RequestParam("orderId") String orderId, @RequestParam("channel") String channel) {
...
// 在链路上下文中添加渠道标签
GlobalTracer.get().activeSpan().setTag("channel", channel);
...
}
serviceName=order AND spanName=createOrder AND duration>5s AND attributes.channel=offline_retail
(四)一个典型的链路诊断示例
本小节我们介绍了三种不同的链路筛选方式,结合上一小节的请求轨迹回溯,我们来看一个典型的链路筛选与诊断过程,主要分为以下几步:
根据 TraceId、应用名、接口名、耗时、状态码、自定义标签等任意条件组合过滤出目标调用链。 从满足过滤条件的调用链列表中选中一条链路查询详情。 结合请求调用轨迹,本地方法栈,主动/自动关联数据(如SQL、业务日志)综合分析调用链。 如果上述信息仍无法定位根因,需要结合内存快照、Arthas 在线诊断等工具进行二次分析。
预告
Aliware
在完整介绍分布式链路追踪的前世今生及基础概念之后,本文了解了请求轨迹回溯、多维链路筛选场景,接下来的章节我们将继续介绍:
链路实时分析、监控与告警 链路拓扑
更多内容,敬请期待!